using System;

namespace NFreeMarker.Core
{
    /**
     * A TemplateElement representing a block of plain text.
     * @version $Id: TextBlock.java,v 1.17 2004/01/06 17:06:42 szegedia Exp $
     */
    public sealed class TextBlock : TemplateElement
    {
        private static readonly char[] EmptyCharArray = new char[0];
        internal static readonly TextBlock EmptyBlock = new TextBlock(EmptyCharArray, false);
        // We're using char[] instead of string for storing the text block because
        // Writer.write(string) involves copying the string contents to a char[] 
        // using string.getChars(), and then calling Writer.write(char[]). By
        // using Writer.write(char[]) directly, we avoid array copying on each 
        // write. 
        private char[] _text;
        private readonly bool _unparsed;

        public TextBlock(string text)
            : this(text, false)
        {
        }

        public TextBlock(string text, bool unparsed)
            : this(text.ToCharArray(), unparsed)
        {
        }

        private TextBlock(char[] text, bool unparsed)
        {
            _text = text;
            _unparsed = unparsed;
        }

        /**
         * Simply outputs the text.
         */
        internal override void Accept(Environment env)
        {
            env.GetOut().Write(_text);
        }

        public override string GetCanonicalForm()
        {
            var text = new string(_text);
            if (_unparsed)
            {
                return "<#noparse>" + text + "</#noparse>";
            }
            return text;
        }

        public override string GetDescription()
        {
            string s = new string(_text).Trim();
            if (s.Length == 0)
            {
                return "whitespace";
            }
            if (s.Length > 20)
            {
                s = s.Substring(0, 20) + "...";
                s = s.Replace('\n', ' ');
                s = s.Replace('\r', ' ');
            }
            return "text block (" + s + ")";
        }

        internal override TemplateElement PostParseCleanup(bool stripWhitespace)
        {
            if (_text.Length == 0) return this;
            int openingCharsToStrip = 0, trailingCharsToStrip = 0;
            bool deliberateLeftTrim = DeliberateLeftTrim();
            bool deliberateRightTrim = DeliberateRightTrim();
            if (!stripWhitespace || _text.Length == 0)
            {
                return this;
            }
            if (Parent.Parent == null && PreviousSibling() == null) return this;
            if (!deliberateLeftTrim)
            {
                trailingCharsToStrip = TrailingCharsToStrip();
            }
            if (!deliberateRightTrim)
            {
                openingCharsToStrip = OpeningCharsToStrip();
            }
            if (openingCharsToStrip == 0 && trailingCharsToStrip == 0)
            {
                return this;
            }
            _text = Substring(_text, openingCharsToStrip, _text.Length - trailingCharsToStrip);
            if (openingCharsToStrip > 0)
            {
                BeginLine++;
                BeginColumn = 1;
            }
            if (trailingCharsToStrip > 0)
            {
                EndColumn = 0;
            }
            return this;
        }

        /**
         * Scans forward the nodes on the same line to see whether there is a 
         * deliberate left trim in effect. Returns true if the left trim was present.
         */
        private bool DeliberateLeftTrim()
        {
            bool result = false;
            for (TemplateElement elem = NextTerminalNode();
                 elem != null && elem.GetBeginLine() == GetEndLine();
                 elem = elem.NextTerminalNode())
            {
                if (elem is TrimInstruction)
                {
                    var ti = (TrimInstruction) elem;
                    if (!ti.Left && !ti.Right)
                    {
                        result = true;
                    }
                    if (ti.Left)
                    {
                        result = true;
                        int lastNewLineIndex = LastNewLineIndex();
                        if (lastNewLineIndex >= 0 || GetBeginColumn() == 1)
                        {
                            char[] firstPart = Substring(_text, 0, lastNewLineIndex + 1);
                            char[] lastLine = Substring(_text, 1 + lastNewLineIndex);
                            if (Trim(lastLine).Length == 0)
                            {
                                _text = firstPart;
                                EndColumn = 0;
                            }
                            else
                            {
                                int i = 0;
                                while (char.IsWhiteSpace(lastLine[i]))
                                {
                                    i++;
                                }
                                char[] printablePart = Substring(lastLine, i);
                                _text = Concat(firstPart, printablePart);
                            }
                        }
                    }
                }
            }
            if (result)
            {
            }
            return result;
        }

        /**
         * Checks for the presence of a t or rt directive on the 
         * same line. Returns true if the right trim directive was present.
         */
        private bool DeliberateRightTrim()
        {
            bool result = false;
            for (TemplateElement elem = PrevTerminalNode();
                 elem != null && elem.GetEndLine() == GetBeginLine();
                 elem = elem.PrevTerminalNode())
            {
                if (elem is TrimInstruction)
                {
                    var ti = (TrimInstruction) elem;
                    if (!ti.Left && !ti.Right)
                    {
                        result = true;
                    }
                    if (ti.Right)
                    {
                        result = true;
                        int firstLineIndex = FirstNewLineIndex() + 1;
                        if (firstLineIndex == 0)
                        {
                            return false;
                        }
                        if (_text.Length > firstLineIndex
                            && _text[firstLineIndex - 1] == '\r'
                            && _text[firstLineIndex] == '\n')
                        {
                            firstLineIndex++;
                        }
                        char[] trailingPart = Substring(_text, firstLineIndex);
                        char[] openingPart = Substring(_text, 0, firstLineIndex);
                        if (Trim(openingPart).Length == 0)
                        {
                            _text = trailingPart;
                            BeginLine++;
                            BeginColumn = 1;
                        }
                        else
                        {
                            int lastNonWs = openingPart.Length - 1;
                            while (char.IsWhiteSpace(_text[lastNonWs]))
                            {
                                lastNonWs--;
                            }
                            char[] printablePart = Substring(_text, 0, lastNonWs + 1);
                            if (Trim(trailingPart).Length == 0)
                            {
                                // THIS BLOCK IS HEINOUS! THERE MUST BE A BETTER WAY! REVISIT (JR)
                                bool trimTrailingPart = true;
                                for (TemplateElement te = NextTerminalNode();
                                     te != null && te.GetBeginLine() == GetEndLine();
                                     te = te.NextTerminalNode())
                                {
                                    if (te.HeedsOpeningWhitespace())
                                    {
                                        trimTrailingPart = false;
                                    }
                                    if (te is TrimInstruction && ((TrimInstruction) te).Left)
                                    {
                                        trimTrailingPart = true;
                                        break;
                                    }
                                }
                                if (trimTrailingPart) trailingPart = EmptyCharArray;
                            }
                            _text = Concat(printablePart, trailingPart);
                        }
                    }
                }
            }
            return result;
        }

        /*
        private string leftTrim(string s) {
            int i =0;
            while (i<s.length()) {
                if (!Character.isWhitespace(s.charAt(i)))
                    break;
                ++i;
            }
            return s.substring(i);
        }
        */

        private int FirstNewLineIndex()
        {
            var content = new string(_text);
            int newlineIndex1 = content.IndexOf('\n');
            int newlineIndex2 = content.IndexOf('\r');
            int result = newlineIndex1 >= 0 ? newlineIndex1 : newlineIndex2;
            if (newlineIndex1 >= 0 && newlineIndex2 >= 0)
            {
                result = Math.Min(newlineIndex1, newlineIndex2);
            }
            return result;
        }

        private int LastNewLineIndex()
        {
            var content = new string(_text);
            return Math.Max(content.LastIndexOf('\r'), content.LastIndexOf('\n'));
        }

        /**
         * figures out how many opening whitespace characters to strip
         * in the post-parse cleanup phase.
         */
        private int OpeningCharsToStrip()
        {
            int newlineIndex = FirstNewLineIndex();
            if (newlineIndex == -1 && BeginColumn != 1)
            {
                return 0;
            }
            ++newlineIndex;
            if (_text.Length > newlineIndex)
            {
                if (newlineIndex > 0 && _text[newlineIndex - 1] == '\r' && _text[newlineIndex] == '\n')
                {
                    ++newlineIndex;
                }
            }
            if (new string(_text).Substring(0, newlineIndex).Trim().Length > 0)
            {
                return 0;
            }
            // We look at the preceding elements on the line to see if we should
            // strip the opening newline and any whitespace preceding it.
            for (TemplateElement elem = PrevTerminalNode();
                 elem != null && elem.GetEndLine() == GetBeginLine();
                 elem = elem.PrevTerminalNode())
            {
                if (elem.HeedsOpeningWhitespace())
                {
                    return 0;
                }
            }
            return newlineIndex;
        }

        /**
         * figures out how many trailing whitespace characters to strip
         * in the post-parse cleanup phase.
         */
        private int TrailingCharsToStrip()
        {
            var content = new string(_text);
            int lastNewlineIndex = LastNewLineIndex();
            if (lastNewlineIndex == -1 && BeginColumn != 1)
            {
                return 0;
            }
            string substring = content.Substring(lastNewlineIndex + 1);
            if (substring.Trim().Length > 0)
            {
                return 0;
            }
            // We look at the elements afterward on the same line to see if we should
            // strip any whitespace after the last newline
            for (TemplateElement elem = NextTerminalNode();
                 elem != null && elem.GetBeginLine() == GetEndLine();
                 elem = elem.NextTerminalNode())
            {
                if (elem.HeedsTrailingWhitespace())
                {
                    return 0;
                }
            }
            return substring.Length;
        }

        internal override bool HeedsTrailingWhitespace()
        {
            if (IsIgnorable())
            {
                return false;
            }
            foreach (char c in _text)
            {
                if (c == '\n' || c == '\r')
                {
                    return false;
                }
                if (!char.IsWhiteSpace(c))
                {
                    return true;
                }
            }
            return true;
        }

        internal override bool HeedsOpeningWhitespace()
        {
            if (IsIgnorable())
            {
                return false;
            }
            for (int i = _text.Length - 1; i >= 0; i--)
            {
                char c = _text[i];
                if (c == '\n' || c == '\r')
                {
                    return false;
                }
                if (!char.IsWhiteSpace(c))
                {
                    return true;
                }
            }
            return true;
        }

        internal override bool IsIgnorable()
        {
            if (_text == null || _text.Length == 0)
            {
                return true;
            }
            if (!IsWhiteSpace())
            {
                return false;
            }
            bool atTopLevel = false; // TODO: (GetParent().getParent() == null);
            TemplateElement prevSibling = PreviousSibling();
            TemplateElement nextSibling = NextSibling();
            return ((prevSibling == null && atTopLevel) || NonOutputtingType(prevSibling))
                   && ((nextSibling == null && atTopLevel) || NonOutputtingType(nextSibling));
        }


        private static bool NonOutputtingType(TemplateElement element)
        {
            return (element is Macro ||
                    element is Assignment ||
                    element is AssignmentInstruction ||
                    element is PropertySetting ||
                    element is LibraryLoad ||
                    element is Comment);
        }

        private static char[] Substring(char[] c, int from, int to)
        {
            var c2 = new char[to - from];
            Array.Copy(c, from, c2, 0, c2.Length);
            return c2;
        }

        private static char[] Substring(char[] c, int from)
        {
            return Substring(c, from, c.Length);
        }

        private static char[] Trim(char[] c)
        {
            if (c.Length == 0)
            {
                return c;
            }
            return new string(c).Trim().ToCharArray();
        }

        private static char[] Concat(char[] c1, char[] c2)
        {
            var c = new char[c1.Length + c2.Length];
            Array.Copy(c1, 0, c, 0, c1.Length);
            Array.Copy(c2, 0, c, c1.Length, c2.Length);
            return c;
        }

        private bool IsWhiteSpace()
        {
            return _text == null || Trim(_text).Length == 0;
        }
    }
}